package com.yingwei.mining.control;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Iterator;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.yingwei.mining.model.businessobject.Configuration;
import com.yingwei.mining.model.businessobject.Device;
import com.yingwei.mining.model.businessobject.DeviceAttachment;
import com.yingwei.mining.model.businessobject.Edge;
import com.yingwei.mining.model.businessobject.Excavator;
import com.yingwei.mining.model.businessobject.Node;
import com.yingwei.mining.model.businessobject.Record;
import com.yingwei.mining.model.businessobject.ReportConfig;
import com.yingwei.mining.model.businessobject.ReportType;
import com.yingwei.mining.model.businessobject.RfidReader;
import com.yingwei.mining.model.businessobject.Route;
import com.yingwei.mining.model.businessobject.TaskRecord;
import com.yingwei.mining.model.businessobject.TaskRecordStatistics;
import com.yingwei.mining.model.businessobject.Tracking;
import com.yingwei.mining.model.businessobject.Truck;
import com.yingwei.mining.model.businessobject.UnloadArea;
import com.yingwei.mining.model.util.HibernateUtil;
import com.yingwei.mining.util.LocationUtil;

public class Data extends AbstractData
{
    private static Logger logger = LoggerFactory.getLogger(Data.class);
    private static Data data;

    private Data()
    {
    }

    public static Data getData()
    {
	if (data == null)
	{
	    data = new Data();
	    data.init();
	    return data;
	}
	return data;
    }

    public synchronized void postInitProcess()
    {
	doCalculatedScreenWidthHeight();
	doUpdateNodeDevice();
	flush();
    }

    public void clear()
    {
	session.clear();
	doUpdateNodeDevice();
    }

    /**
     * this shall only be called when there is node location change or route
     * change
     */
    @SuppressWarnings("unchecked")
    public synchronized void reCalculateAllExistingRoutes()
    {
	List<Route> routes = (List<Route>) session.createQuery("from Route")
		.list();
	for (Route route : routes)
	{
	    NodeWrapper dest = findShortestRoute(route.getNodeFrom(), route
		    .getNodeTo());
	    if (dest != null && dest.value != route.getValue())
	    {
		route.setValue(dest.value);
		route.setEdges(dest.routeEdge);
		route.setModifyTime(new Date());
		session.merge(route);
		session.flush();
		// updateRoute(route);
	    }
	}
    }

    public void doCalculatedScreenWidthHeight()
    {
	/**
	 * calculate calculalatedScreenWidth/Height in configuration
	 */
	Configuration configuration = getConfiguration();
	int x = configuration.getNorthWestLongitude();
	int y = configuration.getNorthWestLatitude();
	int x1 = configuration.getSouthEastLongitude();
	int y1 = configuration.getSouthEastLatitude();
	double screenWidth = ((x1 - x) / 100.0) / configuration.getMmPerPixel()
		* 6 * configuration.getMeterPerNauticalMile();
	double screenHeight = ((y - y1) / 100.0)
		/ configuration.getMmPerPixel() * 6
		* configuration.getMeterPerNauticalMile();
	// update calculated area
	configuration.setCalculatedScreenWidth((int) screenWidth);
	configuration.setCalculatedScreenHeight((int) screenHeight);
	session.flush();
    }

    private void doUpdateNodeDevice()
    {
	/**
	 * post processing. device can't be instanced or narrowed to excavator,
	 * truck or unloadarea because hibernate used proxy objects. device
	 * field of node needs to be set up in the following code:
	 */
	List<Node> nodes = getNodes();
	List<Excavator> excavators = getExcavators();
	List<UnloadArea> unloadAreas = getUnloadAreas();
	for (int i = 0; i < nodes.size(); i++)
	{
	    Device d = nodes.get(i).getDevice();
	    if (d != null)
	    {
		Excavator ex = (Excavator) findObject(excavators, d.getId());
		if (ex != null)
		{
		    nodes.get(i).setDevice(ex);
		}
		else
		{
		    UnloadArea un = (UnloadArea) findObject(unloadAreas, d
			    .getId());
		    if (un != null)
		    {
			nodes.get(i).setDevice(un);
		    }
		    else
		    {
			logger.error("something is wrong with node link: "
				+ d.getId());
		    }
		}
	    }
	}
    }

    /**
     * This shall be the only interface for routes.
     */
    synchronized public Route calculateRoute(Node from, Node to)
    {
	Route result = null;
	for (Route route : getRoutes())
	{
	    if (from.getId() == route.getNodeFrom().getId()
		    && to.getId() == route.getNodeTo().getId())
	    {
		result = route;
		break;
	    }
	}
	if (result == null)
	{
	    NodeWrapper dest = findShortestRoute(from, to);
	    if (dest == null)
	    {
		result = new Route(from, to, 0, null, new Date(), new Date());
	    }
	    else
	    {
		result = new Route(from, to, dest.value, dest.routeEdge,
			new Date(), new Date());
	    }
	    updateRoute(result);
	    flush();
	}
	return result;
    }

    private void updateRoute(Route rt)
    {
	// update database table when value changed (node or route changed)
	boolean isExist = false;
	for (Route rtdb : getRoutes())
	{
	    if (rt.getNodeFrom().getId() == rtdb.getNodeFrom().getId()
		    && rt.getNodeTo().getId() == rtdb.getNodeTo().getId())
	    {
		isExist = true;
		if (rt.getValue().intValue() != rtdb.getValue().intValue())
		{
		    logger.debug("Route updated. New value:" + rt.getValue()
			    + " Old value:" + rtdb.getValue()
			    + "for route from:" + rt.getNodeFrom().getId()
			    + " to:" + rt.getNodeTo().getId());
		    rtdb.setEdges(rt.getEdges());
		    rtdb.setValue(rt.getValue());
		    rtdb.setModifyTime(new Date());
		    session.merge(rtdb);
		    break;
		}
	    }
	}
	// add a new one if necessary
	if (!isExist)
	{
	    session.save(rt);
	}
    }

    @SuppressWarnings("unchecked")
    private Device findObject(List arr, long id)
    {
	for (int i = 0; i < arr.size(); i++)
	{
	    Device d = (Device) arr.get(i);
	    if (d.getId() == id)
	    {
		return d;
	    }
	}
	return null;
    }

    class NodeWrapper
    {
	boolean resolved;
	int value;
	Node node;
	Set<Edge> routeEdge;

	NodeWrapper(boolean resolved, int value, Node node)
	{
	    this.resolved = resolved;
	    this.value = value;
	    this.node = node;
	    this.routeEdge = new HashSet<Edge>();
	}

	int calculateDistance(Node node)
	{
	    int dx = this.node.getX() - node.getX();
	    int dy = this.node.getY() - node.getY();
	    double distance = Math.sqrt(dx * dx + dy * dy);
	    return (int) distance;
	}
    }

    protected Node findNodeByDevice(Device device)
    {
	for (Node node : this.getNodes())
	{
	    if (node.getDevice() != null
		    && node.getDevice().getId() == device.getId())
		return node;
	}
	return null;
    }

    /**
     * 
     * @param from
     * @param to
     *            dijkstra algorithm
     * @return
     */
    protected NodeWrapper findShortestRoute(Node nodeFrom, Device device)
    {
	for (Node node : this.getNodes())
	{
	    if (node.getDevice() != null
		    && node.getDevice().getId() == device.getId())
		return findShortestRoute(nodeFrom, node);
	}
	return null;
    }

    private NodeWrapper findShortestRoute(Node nodeFrom, Node nodeTo)
    {
	List<NodeWrapper> resolved = new ArrayList<NodeWrapper>();
	Map<Long, NodeWrapper> all = new HashMap<Long, NodeWrapper>();
	for (Node node : getNodes())
	{
	    if (node.getId() == nodeFrom.getId())
	    {
		NodeWrapper startingNode = new NodeWrapper(true, 0, node);
		resolved.add(startingNode);
		all.put(node.getId(), startingNode);
	    }
	    else
	    {
		all.put(node.getId(), new NodeWrapper(false, Integer.MAX_VALUE,
			node));
	    }
	}
	return findNextShortestNeighbor(all, resolved, nodeTo);
    }

    /**
     * this newer version that assume one way direction. 2 links between 2
     * nodes.
     * 
     * @param all
     * @param resolved
     * @param target
     * @return
     */
    private NodeWrapper findNextShortestNeighbor(Map<Long, NodeWrapper> all,
	    List<NodeWrapper> resolved, Node target)
    {
	int smallestValue = Integer.MAX_VALUE;
	NodeWrapper smallestNodeWrapper = null;
	for (NodeWrapper resolvedNode : resolved)
	{
	    for (Edge edge : resolvedNode.node.getFromEdges())
	    {
		Node nextNode = edge.getToNode();
		// logger.debug("resolved:" + resolvedNode.node.getId() +
		// " edge.getToNode():" + edge.getToNode().getId());
		NodeWrapper nextNodeWrapper = all.get(nextNode.getId());
		if (!nextNodeWrapper.resolved)
		{
		    int dist = resolvedNode.calculateDistance(nextNode);
		    if (resolvedNode.value + dist <= nextNodeWrapper.value)
		    {
			nextNodeWrapper.value = resolvedNode.value + dist;
			nextNodeWrapper.routeEdge.clear();
			for (Edge routeEdge : resolvedNode.routeEdge)
			{
			    nextNodeWrapper.routeEdge.add(routeEdge);
			}
			nextNodeWrapper.routeEdge.add(edge);
		    }
		    if (nextNodeWrapper.value <= smallestValue)
		    {
			smallestValue = nextNodeWrapper.value;
			smallestNodeWrapper = nextNodeWrapper;
		    }
		}
	    }
	}
	if (smallestNodeWrapper == null)
	{
	    return null;
	}
	smallestNodeWrapper.resolved = true;
	// logger.debug("resolved size:" + resolved.size());
	resolved.add(smallestNodeWrapper);
	// logger.debug("resolved size:" + resolved.size());
	// logger.debug("resolved:" + smallestNodeWrapper.node.getId() +
	// " value:"
	// + smallestNodeWrapper.value
	// + smallestNodeWrapper.routeEdge.toString());
	/*
	 * String resolvedNodeString = ""; for (NodeWrapper n : resolved) {
	 * resolvedNodeString += n.node.getId() + ","; }
	 * logger.debug("resolved nodes:" + resolvedNodeString);
	 */
	if (smallestNodeWrapper.node.getId() == target.getId())
	{
	    return smallestNodeWrapper;
	}
	else
	{
	    return findNextShortestNeighbor(all, resolved, target);
	}
    }

    /**
     * Find if the node has the device attached or linked.
     * 
     * @param d
     * @return
     */
    public boolean isDeviceAttached(Device d)
    {
	for (Node nd : getNodes())
	{
	    if (nd.getDevice() != null && nd.getDevice().getId() == d.getId())
	    {
		return true;
	    }
	}
	return false;
    }

    public Excavator castToExcavator(Device d)
    {
	for (Excavator ex : getExcavators())
	{
	    if (ex.getId() == d.getId())
	    {
		return ex;
	    }
	}
	return null;
    }

    public UnloadArea castToUnloadArea(Device d)
    {
	for (UnloadArea ex : getUnloadAreas())
	{
	    if (ex.getId() == d.getId())
	    {
		return ex;
	    }
	}
	return null;
    }

    public Truck castToTruck(Device d)
    {
	for (Truck tr : getTrucks())
	{
	    if (tr.getId() == d.getId())
	    {
		return tr;
	    }
	}
	return null;
    }

    /**
     * make edges bi-directional
     */
    private void duplicateReturnEdges()
    {
	for (Edge ed : getEdges())
	{
	    Edge edge = new Edge();
	    edge.setFromNode(ed.getToNode());
	    edge.setToNode(ed.getFromNode());
	    edge.setCreateTime(new Date());
	    edge.setModifyTime(new Date());
	    session.save(edge);
	}
    }

    public static void main1(String[] args)
    {
	Data data = new Data();
	data.init();
	data.postInitProcess();
	Node from = (Node) data.load(Node.class, new Long(54));
	Node to = (Node) data.load(Node.class, new Long(16));
	data.calculateRoute(from, to);
	data.flush();
	data.calculateRoute(to, from);
	data.flush();
    }

    public List<Object> getStatisticPerTruck(String from, String to)
    {
	// TODO Auto-generated method stub
	return null;
    }

    @SuppressWarnings("unchecked")
    public List<TaskRecord> getTaskRecords(String filterTruck, String from,
	    String to, boolean filterValidJob)
    {
	String filterString = "select taskRecord from TaskRecord taskRecord, Truck truck where taskRecord.device = truck.id ";
	if (filterTruck != null && !filterTruck.trim().isEmpty())
	{
	    filterString += " and truck.name = '" + filterTruck + "'";
	}
	// if (filter_station != null && !filter_station.trim().isEmpty())
	{
	    // filterString += " and record.station = " + filter_station;
	}
	// DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	if (from != null && !from.trim().isEmpty())
	{
	    filterString += " and taskRecord.startTime >= '" + from + "'";
	}
	if (to != null && !to.trim().isEmpty())
	{
	    filterString += " and taskRecord.endTime <= '" + to + "'";
	}
	if (filterValidJob)
	{
	    filterString += " and taskRecord.taskDefine.taskType = '"
		    + Constant.TASKTYPE_VALID + "'";
	}
	List result = com.yingwei.mining.util.HibernateUtil.getSessionFactory()
		.getCurrentSession().createQuery(filterString).list();
	return result;
    }

    @SuppressWarnings( { "unchecked", "deprecation" })
    public List<TaskRecordStatistics> getTaskRecordsStatistic(String from,
	    String to, boolean filterValidJob)
    {
	String query = "SELECT task_record.id id, device.name plate,task_define.name task,task_define.nodes note,task_type.name valid,count(*) count FROM task_record, device, task_define, task_type where ";
	if (from != null && from.trim().length() > 0)
	{
	    query += "task_record.start_time >= '" + from.trim() + "' and ";
	}
	if (to != null && to.trim().length() > 0)
	{
	    query += "task_record.end_time <= '" + to.trim() + "' and ";
	}
	if (filterValidJob)
	{
	    query += "task_type_id = '" + Constant.TASKTYPE_VALID + "' and ";
	}
	query += "task_define.task_type_id = task_type.id and device.id = task_record.device_id and task_record.task_define_id = task_define.id group by device_id, task_define_id, task_type_id order by device_id,task_type_id desc";
	List<TaskRecordStatistics> result = com.yingwei.mining.util.HibernateUtil
		.getSessionFactory().getCurrentSession().createSQLQuery(query)
		.addEntity(TaskRecordStatistics.class).list();
	return result;
    }

    @SuppressWarnings("unchecked")
    public List<ReportConfig> getUnrunReportConfigs()
    {
	Date now = new Date();
	List result = session.createQuery(
		"from ReportConfig where been_run = false and to_time < ?")
		.setParameter(0, now).list();
	return result;
    }

    public Tracking createTracking(String gpsId, int x, int y, int z,
	    int direction, int speed, Date date)
    {
	if (gpsId != null)
	{
	    for (DeviceAttachment da : getDeviceAttachments())
	    {
		if (gpsId.trim().equalsIgnoreCase(da.getGpsId().trim()))
		{
		    Device device = da.getDevice();
		    List<Tracking> lst = getSortedTrackings(device);
		    Tracking checkTracking = lst.get(0);
		    if (checkTracking.getX() == x && checkTracking.getY() == y)
		    {
			checkTracking.setModifyTime(date);
			updateTracking(checkTracking);
			logger.debug("createdxxxx tracking - update is done");
			return checkTracking;
		    }
		    else
		    {
			Tracking tracking = new Tracking();
			tracking.setDevice(device);
			tracking.setX(x);
			tracking.setY(y);
			tracking.setZ(z);
			tracking.setDirection(direction);
			tracking.setSpeed(speed);
			tracking.setCreateTime(date);
			tracking.setModifyTime(date);
			// tracking.setHeartBeatTime(date);
			logger.debug("createdxxxx tracking");
			createTracking(tracking);
			return tracking;
		    }
		}
	    }
	}
	return null;
    }

    public Record createRecord(String rfidId, String stationIp, Date date)
    {
	logger.debug("createRecord rfid: " + rfidId + " stationIp:" + stationIp);
	if (rfidId != null && stationIp != null)
	{
	    for (DeviceAttachment da : getDeviceAttachments())
	    {
		if (rfidId.trim().equalsIgnoreCase(da.getRfid1().trim())
			|| rfidId.trim().equalsIgnoreCase(da.getRfid2().trim())
			|| rfidId.trim().equalsIgnoreCase(da.getRfid3().trim()))
		{
		    Device device = da.getDevice();
		    for (RfidReader rr : getRfidReaders())
		    {
			if (stationIp.trim().equalsIgnoreCase(rr.getIp()))
			{
			    Record record = new Record();
			    record.setDevice(device);
			    record.setNode(rr.getNode());
			    record.setCreateTime(new Date());
			    record.setModifyTime(record.getCreateTime());
			    record.setNote("rfid");
			    // record.setHeartBeatTime( );
			    logger.debug("createdxxxx record");
			    createRecord(record);
			    return record;
			}
		    }
		}
	    }
	}
	return null;
    }

    public void updateTracking(String gpsId, Date heartBeatTime)
    {
	if (gpsId != null && heartBeatTime != null)
	{
	    for (DeviceAttachment da : getDeviceAttachments())
	    {
		if (gpsId.trim().equalsIgnoreCase(da.getGpsId().trim()))
		{
		    List<Tracking> lst = getSortedTrackings(da.getDevice());
		    Tracking tracking = lst.get(0);
		    tracking.setHeartBeatTime(heartBeatTime);
		    tracking.setModifyTime(heartBeatTime);
		    this.updateTracking(tracking);
		    logger.debug("update tracking heartbeat "
			    + heartBeatTime.toString());
		    updateTracking(tracking);
		    return;
		}
	    }
	}
    }
}
